package com.general.client.widgets.combobox;

import java.util.ArrayList;
import java.util.HashMap;

import com.general.client.action.ActionHandler;
import com.general.client.basewidget.BaseWidget;
import com.general.client.bundle.CommonBundle;
import com.general.client.callback.Callback;
import com.general.client.enableable.Enableable;
import com.general.client.error.Error;
import com.general.client.hasinterfaces.HasHint;
import com.general.client.hasinterfaces.HasLabel;
import com.general.client.mandatory.Mandatory;
import com.general.client.modes.HasModes;
import com.general.client.modes.WidgetMode;
import com.general.client.position.Position;
import com.general.client.validator.Validateable;
import com.general.client.widgets.autopositionpanel.AutoPositionPanel;
import com.general.client.widgets.button.combobox.ComboboxButton;
import com.general.client.widgets.iterablelist.IterableList;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;

public class Combobox<T> extends BaseWidget implements IComboBox<T>, HasModes,
		Enableable, HasHint, Validateable, HasValue<T>, HasLabel, Mandatory,
		Error {

	private static ComboBoxUiBinder uiBinder = GWT
			.create(ComboBoxUiBinder.class);

	interface ComboBoxUiBinder extends UiBinder<Widget, Combobox<?>> {
	}

	@UiField
	FlowPanel buttonContainer;

	@UiField
	Label buttonView;

	@UiField
	ComboboxButton button;

	@UiField
	AutoPositionPanel autoPositionPanel;

	@UiField
	IterableList iterableList;

	@UiField
	Label astrix;

	@UiField
	Label label;

	@UiField
	FlowPanel labelContainer;

	@UiField
	CommonBundle bundle;

	private final HashMap<T, String> mapper = new HashMap<T, String>();

	private final ArrayList<T> valuesList = new ArrayList<T>();

	private final String NON_SELECTED = "";

	private boolean showLabel = true;

	private Position labelPosition = Position.LEFT;

	private boolean error = false;

	private boolean isMandatory = false;

	private ArrayList<Callback<T>> selectedItemChangedCallback;

	private boolean isListVisible = false;

	private WidgetMode mode = WidgetMode.EDIT;

	private HandlerRegistration onClickSwitchToEditRegistration;

	private HandlerRegistration onBlurSwitchToViewRegistration;

	private String hint = NON_SELECTED;

	private boolean isShowHint = false;

	private HandlerRegistration addValueChangeHandlerRegistration;

	public Combobox() {
		initWidget(uiBinder.createAndBindUi(this));
		autoPositionPanel.setTabIndex(-1);
		autoPositionPanel.setRelativeWidget(button);
		autoPositionPanel.isWidthAsRelativeWidget(true);
		// label defaults to show, left
		setLabelPosition(Position.LEFT);
		setShowLabel(true);

		// non mandatory by default
		setMandatory(false);

		setListVisible(isListVisible);
		setSelected(iterableList.getSelectedIndex());
		setMode(mode);
		// add idle style
		iterableList.addIdleStyleName(bundle.style().iterableListIdleItem());
		// add highlighting style
		iterableList.addHighlightStyleName(bundle.style()
				.iterableListHighlightedItem());
		// add selecting style
		iterableList.addSelectionStyleName(bundle.style()
				.iterableListSelectedItem());

		// add action on items to close the list
		iterableList.addActionHandler(new ActionHandler() {

			@Override
			public <H extends EventHandler> void onAction(GwtEvent<H> event) {
				if (isEnabled()) {
					setListVisible(false);
				}
			}
		});
		// add blur handler on list to hide list
		iterableList.addBlurHandler(new BlurHandler() {

			@Override
			public void onBlur(BlurEvent event) {
				setListVisible(false);
				T selected = getSelected();
				if (null == selected) {
					showEmptyText();
				} else {
					button.getText().setText(mapper.get(selected));
					buttonView.setText(mapper.get(selected));
				}
			}
		});
		// add selection change item on list to trigger callback, and hide list,
		// and change value of button
		iterableList.onSelectionChanged(new Callback<Integer>() {

			@Override
			public void onItemChanged(Integer item) {
				setListVisible(false);
				T selected = getSelected();
				if (null == selected) {
					showEmptyText();
				} else {
					button.getText().setText(mapper.get(selected));
					buttonView.setText(mapper.get(selected));
				}
				callOnSelectionCallbacks(selected);
			}
		});
		// add highlight change on the list to change value of button
		iterableList.onHighlightedChanged(new Callback<Integer>() {

			@Override
			public void onItemChanged(Integer item) {
				T highlighted = (item == -1) ? null : valuesList.get(item);
				if (null != highlighted) {
					button.getText().setText(mapper.get(highlighted));
					buttonView.setText(mapper.get(highlighted));
				}
				// dont show the non selected 'cause it just bugs the eyes
				// else {
				// button.getText().setText(NON_SELECTED + "1");
				// }
			}
		});

		// add actions on button to show\hide list
		button.addActionHandler(new ActionHandler() {

			@Override
			public <H extends EventHandler> void onAction(GwtEvent<H> event) {
				if (isEnabled()) {
					setListVisible(!isListVisible);
				}

			}
		});
		// add handlers on button to open the list and move focus to it (enter,
		// up, down)
		// esc to close the list
		button.addKeyDownHandler(new KeyDownHandler() {

			@Override
			public void onKeyDown(KeyDownEvent event) {
				switch (event.getNativeKeyCode()) {
				case KeyCodes.KEY_ENTER:
				case KeyCodes.KEY_UP:
				case KeyCodes.KEY_DOWN:
					if (isEnabled()) {
						setListVisible(true);
					}
					break;
				case KeyCodes.KEY_ESCAPE:
				case KeyCodes.KEY_TAB:
					setListVisible(false);
					break;
				default:
					break;
				}
			}
		});

	}

	protected void callOnSelectionCallbacks(T selected) {
		if (selectedItemChangedCallback == null
				|| selectedItemChangedCallback.isEmpty())
			return;
		for (Callback<T> callback : selectedItemChangedCallback) {
			callback.onItemChanged(selected);
		}

	}

	protected void setListVisible(boolean b) {
		isListVisible = b;
		iterableList.setFocus(b);
		iterableList.setVisible(b);
		autoPositionPanel.setVisible(b);
		if (isListVisible) {
			button.setDown(true);
			iterableList.setFocus(b);
		} else {
			button.setFocus();
			autoPositionPanel.setFocus(false);
			button.setDown(false);
		}
	}

	@Override
	/**
	 * multiple items with same value are NOT allowed 
	 * adding multiple items with same value will ignore all but first
	 * @param value - value of item
	 * @param label - label to represent the value
	 */
	public void addItem(T value, String label) {
		if (valuesList.contains(value))
			return;
		mapper.put(value, label);
		valuesList.add(value);
		Label itemLabel = new Label();
		itemLabel.setText(label);
		iterableList.add(itemLabel);
	}

	/**
	 * set selected item
	 * 
	 * @param value
	 *            if value not in list - will do nothing null to to select none
	 */
	@Override
	public void setSelected(T value) {
		if (value == null) {
			iterableList.setSelectedIndex(-1);
			showEmptyText();
		} else if (valuesList.contains(value)) {
			iterableList.setSelectedIndex(valuesList.indexOf(value));
			button.getText().setText(mapper.get(value));
			buttonView.setText(mapper.get(value));
		}
	}

	/**
	 * set selected item
	 * 
	 * @param index
	 *            - index of item to select. -1 to select none. index out of
	 *            range (except -1) will do nothing
	 */
	@Override
	public void setSelected(int index) {
		if (index < -1 || index >= valuesList.size())
			return;
		iterableList.setSelectedIndex(index);
		if (index == -1) {
			showEmptyText();
		} else {
			button.getText().setText(mapper.get(valuesList.get(index)));
			buttonView.setText(mapper.get(valuesList.get(index)));
		}
	}

	private void showEmptyText() {
		String emptyText = NON_SELECTED;
		if (isShowHint) {
			emptyText = hint;
		}
		button.getText().setText(emptyText);
		buttonView.setText(emptyText);

	}

	/**
	 * get selected value
	 * 
	 * @return selected value. null if none selected \ empty list
	 */
	@Override
	public T getSelected() {
		int i = iterableList.getSelectedIndex();
		if (i == -1)
			return null;
		return valuesList.get(i);
	}

	/**
	 * get the index of currently selected value
	 * 
	 * @return index of currently selected value. -1 if none selected \ empty
	 *         list
	 */
	@Override
	public int getSelectedIndex() {
		return iterableList.getSelectedIndex();
	}

	/**
	 * will be called when selected item is changed called even on setSelected
	 * 
	 * @param callback
	 */
	@Override
	public void onSelectedItemChanged(Callback<T> callback) {
		if (selectedItemChangedCallback == null) {
			selectedItemChangedCallback = new ArrayList<Callback<T>>();
		}
		this.selectedItemChangedCallback.add(callback);
	}

	private void removeCallback(Callback<T> callback) {
		if (selectedItemChangedCallback == null)
			return;
		selectedItemChangedCallback.remove(callback);
	}

	@Override
	public WidgetMode getMode() {
		return this.mode;
	}

	@Override
	public void setMode(WidgetMode mode) {
		this.mode = mode;
		setListVisible(false);
		setSelected(iterableList.getSelectedIndex());
		if (mode.equals(WidgetMode.EDIT)) {
			button.setVisible(true);
			buttonView.setVisible(false);
			button.setFocus();
		} else {
			buttonView.setVisible(true);
			button.setVisible(false);
		}
	}

	@Override
	public void setOnClickSwitchToEdit(boolean onFocusSwitchToEdit) {
		if (onFocusSwitchToEdit && onClickSwitchToEditRegistration == null) {
			onClickSwitchToEditRegistration = buttonView
					.addClickHandler(new ClickHandler() {

						@Override
						public void onClick(ClickEvent event) {
							if (isEnabled()) {
								setMode(WidgetMode.EDIT);
								setListVisible(true);
							}
						}
					});
		} else if (onClickSwitchToEditRegistration != null) {
			onClickSwitchToEditRegistration.removeHandler();
		}

	}

	@Override
	public void setOnBlurSwitchToView(boolean onBlurSwitchToView) {
		if (onBlurSwitchToView && onBlurSwitchToViewRegistration == null) {
			onBlurSwitchToViewRegistration = iterableList
					.addBlurHandler(new BlurHandler() {

						@Override
						public void onBlur(BlurEvent event) {
							setMode(WidgetMode.VIEW);
						}
					});
		} else if (onBlurSwitchToViewRegistration != null) {
			onBlurSwitchToViewRegistration.removeHandler();
		}
	}

	@Override
	public boolean isEnabled() {
		return button.isEnabled();
	}

	@Override
	public void setEnabled(boolean isEnabled) {
		button.setEnabled(isEnabled);
		if (!isEnabled) {
			setListVisible(false);
		}
		if (isEnabled) {
			button.removeStyleName(bundle.style().disabled());
			buttonView.removeStyleName(bundle.style().disabled());
		} else {
			button.addStyleName(bundle.style().disabled());
			buttonView.addStyleName(bundle.style().disabled());
		}

	}

	@Override
	public void setHint(String hint) {
		this.hint = hint;
		if (getSelected() == null) {
			showEmptyText();
		}
	}

	@Override
	public boolean isShowHint() {
		return this.isShowHint;
	}

	@Override
	public void setShowHint(boolean showHint) {
		this.isShowHint = showHint;
		if (getSelected() == null) {
			showEmptyText();
		}
	}

	/**
	 * Label methods
	 */
	@Override
	public String getLabel() {
		return label.getText();
	}

	@Override
	public void setLabel(String labelString) {
		label.setText(labelString);
	}

	@Override
	public boolean isShowLabel() {
		return showLabel;
	}

	@Override
	public void setShowLabel(boolean showLabel) {
		this.showLabel = showLabel;
		labelContainer.setVisible(showLabel);
	}

	/**
	 * label position methods
	 */
	@Override
	public Position getLabelPosition() {
		return labelPosition;
	}

	@Override
	public void setLabelPosition(Position labelPosition) {
		this.labelPosition = labelPosition;
		if (labelPosition.equals(Position.ABOVE)) {
			button.addStyleName(bundle.style().positionAbove());
			buttonView.addStyleName(bundle.style().positionAbove());
		} else if (labelPosition.equals(Position.LEFT)) {
			button.addStyleName(bundle.style().positionLeft());
			buttonView.addStyleName(bundle.style().positionLeft());
		}
	}

	/**
	 * error mode methods
	 */
	@Override
	public boolean isError() {
		return error;
	}

	@Override
	public void setError(boolean error) {
		this.error = error;
		if (error) {
			if (mode.equals(WidgetMode.EDIT)) {
				button.setFocus();
			}
			button.addStyleName(bundle.style().error());
			buttonView.addStyleName(bundle.style().error());
		} else {
			button.removeStyleName(bundle.style().error());
			buttonView.removeStyleName(bundle.style().error());
		}
	}

	@Override
	public void setMandatory(boolean isMandatory) {
		this.isMandatory = isMandatory;
		astrix.setVisible(isMandatory);
	}

	@Override
	public boolean isMandatory() {
		return isMandatory;
	}

	@Override
	public boolean isValid() {
		boolean isValid = (getSelected() != null && isMandatory || !isMandatory);
		setError(!isValid);
		return isValid;
	}

	@Override
	public void setValidateOnValueChange(boolean validateOnValueChange) {
		if (validateOnValueChange && addValueChangeHandlerRegistration == null) {
			addValueChangeHandlerRegistration = addValueChangeHandler(new ValueChangeHandler<T>() {

				@Override
				public void onValueChange(ValueChangeEvent<T> event) {
					isValid();
				}
			});
		} else if (addValueChangeHandlerRegistration != null) {
			addValueChangeHandlerRegistration.removeHandler();
			addValueChangeHandlerRegistration = null;
		}
	}

	@Override
	public HandlerRegistration addValueChangeHandler(
			final ValueChangeHandler<T> handler) {
		final Callback<T> callback = new Callback<T>() {

			@Override
			public void onItemChanged(T item) {
				handler.onValueChange(new ValueChangeEvent<T>(item) {
				});
			}
		};
		onSelectedItemChanged(callback);
		return new HandlerRegistration() {

			@Override
			public void removeHandler() {
				removeCallback(callback);
			}
		};
	}

	@Override
	public T getValue() {
		return getSelected();
	}

	@Override
	public void setValue(T value) {
		setSelected(value);
	}

	@Override
	/**
	 * does not fire events, ever.
	 */
	public void setValue(T value, boolean fireEvents) {
		setValue(value);
	}

	@Override
	public void setWidth(String width) {
		button.setWidth(width);
	}

	@Override
	public void setMaxHeight(String maxHeight) {
		iterableList.getElement().setAttribute("style",
				"max-height:" + maxHeight + "; overflow-y: scroll;");
	}

}
